import { Function, Predicate } from '../Function'
import { ArrayTool } from './ArrayTool'

/**
 * TreeTool
 * @author 冰凝
 * @date 2022-09-19 09:45:27
 **/
export class TreeTool {

    /**
     * 根据指定子节点, 递归寻找所在树中 从根节点到指定节点路径上所有节点
     * @template {Constructor<{}>} NODE
     * @param data {NODE | NODE[]}树
     * @param currentNode {NODE} 目标节点
     * @param id {string} 判断是否相等的属性名, 默认 id
     * @param child {string} 子节点属性名, 默认 children
     * @return {NODE[]}
     */
    public static treePath<T>(data: T | T[], currentNode: T, id = 'id', child = 'children') {
        const pathArr: T[] = []
        let find = false

        const recursionTree = (treeData: T | T[]) => {
            if (find) {
                return
            }
            // 数组
            if (Array.isArray(treeData)) {
                for (const treeDataItem of treeData) {
                    recursionTree(treeDataItem)
                    if (find) {
                        return
                    }
                }
                return
            }

            // 对象
            // @ts-ignore
            if (treeData[id] === currentNode[id]) {
                find = true
                pathArr.push(treeData as any)
                return
            }
            // 树节点
            // @ts-ignore
            const childList = treeData[child]
            if (Array.isArray(childList) && childList.length > 0) {
                for (const c of childList) {
                    // 深度 +1
                    pathArr.push(treeData)
                    recursionTree(c)
                    if (find) {
                        return
                    } else {
                        pathArr.pop()
                    }
                }
            }
        }
        recursionTree(data)

        return pathArr
    }

    /**
     * 树结构遍历
     * @param treeArr 源树结构数组
     * @param map 需要对节点元素执行操作的映射器
     * @param {[] | null} emptyChildren 空子节点使用什么值代替, 默认 []
     * @param children Children 属性名, 默认 children
     * @returns 映射器返回的结果的数组
     */
    public static treeEach<T, R>(treeArr: Array<T>, map: Function<T, R>, emptyChildren: [] | null = [], children?: keyof T): Array<R> {
        if (ArrayTool.isEmpty(treeArr)) {
            return emptyChildren as any
        }
        return treeArr.map(i => {
            const propertyName = children || 'children'
            // @ts-ignore
            if (this.isNotEmpty(i[propertyName])) {
                // @ts-ignore
                i[propertyName] = this.treeEach(i[propertyName], map, emptyChildren, children)
            }
            return map(i)
        })
    }

    /**
     * 构建树结构
     * @param treeList 展开的节点列表
     * @param filterRoot 过滤根节点的策略
     * @param idKey ID 属性名
     * @param pidKey parentId 属性名
     * @param childrenKey children 列表 属性名
     * @param leaveEmptyChildren 是否保留空的子节点, 默认不保留
     */
    public static buildTree<E, I extends keyof E, P extends keyof E, C extends keyof E>(
        treeList: Array<E>,
        filterRoot: Predicate<E>,
        idKey: I,
        pidKey: P,
        childrenKey: C,
        leaveEmptyChildren: boolean = false,
    ): Array<E> {
        if (ArrayTool.isEmpty(treeList)) {
            return []
        }
        // Id -> 节点映射
        const mapping: Record<PropertyKey, E> = treeList.reduce((pv, cv) => {
            pv[cv[idKey] as any] = cv
            return pv
        }, <Record<PropertyKey, E>> {})

        for (let node of treeList) {
            const pid = node[pidKey] as any
            if (pid === undefined) {
                continue
            }
            const pNode = mapping[pid]
            if (pNode === undefined || pNode === null) {
                continue
            }
            if (ArrayTool.isEmpty(pNode[childrenKey] as any)) {
                pNode[childrenKey] = [] as any
            }
            ( pNode[childrenKey] as any as Array<E> ).push(node)
        }

        // @ts-ignore
        let root: Array<E> = Object.values(mapping).filter(filterRoot)

        if (!leaveEmptyChildren) {
            root = ArrayTool.treeEach(root, i => {
                if (ArrayTool.isEmpty(i[childrenKey] as any)) {
                    delete i[childrenKey]
                }
                return i
            }, undefined, childrenKey)
        }
        return root
    }

}
